package br.edu.unifei.mestrado.db.graph;

import java.io.File;
import java.util.Iterator;

import org.neo4j.graphdb.DynamicRelationshipType;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.kernel.Traversal;
import org.neo4j.util.GraphDatabaseUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.edu.unifei.mestrado.commons.graph.GraphProperties;
import br.edu.unifei.mestrado.commons.graph.GraphWrapper;
import br.edu.unifei.mestrado.commons.graph.NodeWrapper;
import br.edu.unifei.mestrado.commons.graph.RelationshipWrapper;
import br.edu.unifei.mestrado.commons.partition.AbstractPartition;

public class GraphDB extends GraphWrapper {

	private Logger logger = LoggerFactory.getLogger(this.getClass());

	// private static final String DB_PATH = "neo4j-";
	private static RelationshipType KNOWS = DynamicRelationshipType.withName("KNOWS");

	private GraphDatabaseService graphDb;
	private Index<Node> indexName;
	private Index<Node> indexPartition;
	private Index<Relationship> indexRelationship;

	private Transaction tx = null;

	private boolean reuseDB = false;

	// private String dbFileName;

	public GraphDB(boolean reuseDB, String dbFileName, Integer level) {
		super(level, dbFileName);
		this.reuseDB = reuseDB;
		// this.dbFileName = dbFileName;
		this.initDB();
	}

	@Override
	public void beginTransaction() {
		tx = graphDb.beginTx();
	}

	@Override
	public void endTransaction() {
		// UTIL: precisa salvar o sizeNodes em um nó especial para poder saber na proxima execução.
		Node referenceNode = graphDb.getReferenceNode();
		referenceNode.setProperty(GraphProperties.SIZE_NODES, sizeNodes);

		tx.success();
	}

	@Override
	public void finish() {
		if (tx != null) {
			tx.finish();
		}
	}

	@Override
	public void shutdown() {
		logger.warn("Shutting down database ...");
		if (graphDb != null) {
			graphDb.shutdown();
		}
	}

	/**
	 * Retorna a aresta que liga os 2 vertices, se existir.
	 * 
	 * TODO: esse método não é eficiente, precisa melhorar.
	 */
	@Override
	public RelationshipWrapper getArestaLinking(NodeWrapper va, NodeWrapper vb) {
		Node na = ((NodeDB) va).getInnerNode();
		Node nb = ((NodeDB) vb).getInnerNode();
		Relationship edge = GraphDatabaseUtil.getExistingRelationshipBetween(na, nb, Traversal
				.expanderForTypes(KNOWS));

		if (edge == null) {
			return null;
		}
		RelationshipWrapper rel = new RelationshipDB(edge);
		return rel;
	}

	private void initDB() {
		String dbFullPath = graphFileName;
		if (getLevel() != null) {
			dbFullPath += "_level-" + getLevel();
		}
		if (!reuseDB) {
			deleteFileOrDirectory(new File(dbFullPath));
		}
		graphDb = new EmbeddedGraphDatabase(dbFullPath);
		indexName = graphDb.index().forNodes("nodes");
		indexPartition = graphDb.index().forNodes("partitions");
		indexRelationship = graphDb.index().forRelationships("relationships");

		registerShutdownHook();
	}

	@Override
	public void readGraph() {
		if (reuseDB) {
			// UTIL: recupera o sizeNodes do nó especial para poder saber a quantidade de nós do banco.
			Node referenceNode = graphDb.getReferenceNode();
			Integer size = (Integer) referenceNode.getProperty(GraphProperties.SIZE_NODES);
			if (size != null) {
				sizeNodes = size;
			}
			return;
		}
		super.readGraph();
	}

	@Override
	public NodeWrapper createNode(long id, int weight) {
		Node node = indexName.get(GraphProperties.ID, id).getSingle();
		if (node == null) {
			sizeNodes++;
			node = graphDb.createNode();
			node.setProperty(GraphProperties.ID, id);
			node.setProperty(GraphProperties.LOCKED, false);
			node.setProperty(GraphProperties.PARTITION, -1);
			node.setProperty(GraphProperties.PESO, weight);

			indexName.add(node, GraphProperties.ID, id);
		}
		return new NodeDB(node);
	}

	@Override
	public NodeWrapper getNode(long id) {
		Node node = indexName.get(GraphProperties.ID, id).getSingle();
		if (node == null) {
			return null;
		}
		return new NodeDB(node);
	}

	@Override
	protected NodeWrapper getOrCreateNode(Long id, int weight) {
		return createNode(id, weight);
	}

	public void printPartitions() {
		logger.debug("{}", new Object() {
			public String toString() {

				StringBuffer b = new StringBuffer();
				IndexHits<Node> part1 = indexPartition.query(GraphProperties.PARTITION, AbstractPartition.PART_1);
				b.append("Vertices from partition 1: ");
				for (Node node : part1) {
					b.append(node.getProperty(GraphProperties.ID) + ",");
				}
				b.append("\n");

				IndexHits<Node> part2 = indexPartition.query(GraphProperties.PARTITION, AbstractPartition.PART_1);
				b.append("Vertices from partition 2: ");
				for (Node node : part2) {
					b.append(node.getProperty(GraphProperties.ID) + ",");
				}
				return b.toString();
			}
		});
	}

	private void registerShutdownHook() {
		// Registers a shutdown hook for the Neo4j instance so that it
		// shuts down nicely when the VM exits (even if you "Ctrl-C" the
		// running example before it's completed)
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				if (getLevel() != null) {
					logger.warn("Shutting down database level " + getLevel() + " ...");
				} else {
					logger.warn("Shutting down database ...");
				}
				graphDb.shutdown();
			}
		});
	}

	private void deleteFileOrDirectory(File file) {
		if (file.exists()) {
			if (file.isDirectory()) {
				for (File child : file.listFiles()) {
					deleteFileOrDirectory(child);
				}
			}
			file.delete();
		}
	}

	public Index<Node> getIndexPartition() {
		return indexPartition;
	}

	public Index<Node> getIndexName() {
		return indexName;
	}

	// TODO: Criar um método que já receba diretamente um objeto nodeWrapper
	@Override
	public RelationshipWrapper createRelationship(long idEdge, int newWeight, long startNode, long endNode) {
		Node firstNode = indexName.get(GraphProperties.ID, startNode).getSingle();
		Node secondNode = indexName.get(GraphProperties.ID, endNode).getSingle();
		if (secondNode == null) {
			logger.error("Verificar |||||||||||||||||||||||||||||||||");
		}

		Relationship rel = firstNode.createRelationshipTo(secondNode, KNOWS);
		indexRelationship.add(rel, GraphProperties.REL, KNOWS.name());
		rel.setProperty(GraphProperties.ID, idEdge);
		rel.setProperty(GraphProperties.PESO, newWeight);

		return new RelationshipDB(rel);
	};

	@Override
	public Iterable<RelationshipWrapper> getAllEdges() {
		return new Iterable<RelationshipWrapper>() {
			@Override
			public Iterator<RelationshipWrapper> iterator() {
				return new Iterator<RelationshipWrapper>() {
					private Iterator<Relationship> edges = indexRelationship.query(GraphProperties.REL,
							KNOWS.name()).iterator();

					@Override
					public RelationshipWrapper next() {
						Relationship edge = edges.next();
						return new RelationshipDB(edge);
					}

					@Override
					public boolean hasNext() {
						return edges.hasNext();
					}

					@Override
					public void remove() {
					}

				};
			}
		};
	}

	@Override
	public Iterable<NodeWrapper> getAllNodes() {

		return new Iterable<NodeWrapper>() {
			@Override
			public Iterator<NodeWrapper> iterator() {
				return new Iterator<NodeWrapper>() {
					private Iterator<Node> nodes = graphDb.getAllNodes().iterator();
					private Node next = null;

					@Override
					public NodeWrapper next() {
						return new NodeDB(next);
					}

					@Override
					public boolean hasNext() {
						if (nodes.hasNext()) {
							next = nodes.next();
							if (next.hasProperty(GraphProperties.ID)) {
								return true;
							} else {
								return this.hasNext();
							}
						}
						return false;
					}

					@Override
					public void remove() {
					}

				};
			}
		};
	}

}
